package com.demo.gulimall.product.service.impl;

import com.alibaba.fastjson.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.demo.gulimall.common.utils.R;
import com.demo.gulimall.product.entity.Category;
import com.demo.gulimall.product.mapper.CategoryMapper;
import com.demo.gulimall.product.service.CategoryService;
import com.demo.gulimall.product.vo.Catelog2Vo;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 商品三级分类
 *
 * @author Chen Yuan
 */

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redisson;

    @Override
    public List<Category> listWithTree() {
        // 查所有
        List<Category> categories = baseMapper.selectList(null);

        // 组装父子结构
        List<Category> level1Menus = categories.stream().filter(c -> Objects.equals(c.getParentCid(), 0L))
                .map(c -> c.setChildren(getChildren(c, categories)))
                .sorted((c1, c2) -> (c1.getSort()==null?0:c1.getSort()) - (c2.getSort()==null?0:c2.getSort()))
                .collect(Collectors.toList());

        return categories;
    }

    /**
     * 递归查到当前菜单的子菜单
     * @param root 当前菜单
     * @param all 所有菜单
     */
    private List<Category> getChildren(Category root, List<Category> all){
        return all.stream().filter(c -> Objects.equals(c.getParentCid(), root.getCatId()))
                .map(c -> c.setChildren(getChildren(c, all)))
                .sorted((c1, c2) -> (c1.getSort()==null?0:c1.getSort()) - (c2.getSort()==null?0:c2.getSort()))
                .collect(Collectors.toList());
    }


    /**
     * @Cacheable
     * 当前方法的结果需要缓存, 如果缓存中有则不调用改方法, 若没有则执行并将返回值缓存
     * cacheNames 指定分区
     */
    @Cacheable(cacheNames = {"category"}, key = "'level1Categorys'", sync = true) // sync = true 使用 Synchronized 对 Get 方法加锁
    @Override
    public List<Category> getLevel1Categorys() {
        System.out.println("getLevel1Categories........");
        long l = System.currentTimeMillis();
        List<Category> categoryEntities = this.baseMapper.selectList(
                new QueryWrapper<Category>().eq("parent_cid", 0));
        System.out.println("消耗时间："+ (System.currentTimeMillis() - l));
        return categoryEntities;
    }


    @Override
    @Cacheable(cacheNames = {"category"}, key = "'level2Categories'")
    public Map<String, List<Catelog2Vo>> getCategoriesJsonFromDb() {
        System.out.println("查询了数据库");

        //将数据库的多次查询变为一次
        List<Category> selectList = this.baseMapper.selectList(null);

        //1、查出所有分类
        //1、1）查出所有一级分类
        List<Category> level1Categorys = getParent_cid(selectList, 0L);

        //封装数据
        return level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<Category> categoryEntities = getParent_cid(selectList, v.getCatId());
            //2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = new ArrayList<>();
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                    //1、找当前二级分类的三级分类封装成vo
                    List<Category> level3Catelog = getParent_cid(selectList, l2.getCatId());

                    if (level3Catelog != null) {
                        List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            return new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(category3Vos);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }

            return catelog2Vos;
        }));
    }

    private List<Category> getParent_cid(List<Category> selectList,Long parentCid) {
        return selectList.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }


    /**
     * 查询分类 + Redis 简单使用
     *
     * ----------
     * 问题一
     *
     * // TODO: 可能会产生对外内存溢出-OutOfDirectMemoryError
     *  产生原因
     *      SpringBoot 2.0 以后默认使用 Lettuce 作为操作 Redis 的客户端, 它使用 Netty 进行网络通信
     *      Lettuce Bug 导致 Netty 堆外内存溢出
     *  解决方案
     *      升级 Lettuce 客户端
     *      切换使用 Jedis
     *
     * ----------
     * 问题二
     *
     * 缓存击穿: 大量请求访问一个不存在的值   —— 解决: 空值也要进行缓存
     * 缓存雪崩: 大量缓存同时失效           —— 解决: 缓存过期时间, 使用随机值
     * 缓存穿透: 热点数据的缓存过期, 此时大量请求查询热点数据 —— 解决: 加锁
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWhitRedis(){
        // 1. 加入缓存
        String catalogJsonKey = "catalogJson";
        String catalogJson = stringRedisTemplate.opsForValue().get(catalogJsonKey);
        Map<String, List<Catelog2Vo>> catalogJsonFromDb;
        if (StringUtils.isNotEmpty(catalogJson)) {
            // 缓存中有就直接使用缓存
            catalogJsonFromDb = JSONObject.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
            return catalogJsonFromDb;
        }

        // 缓存中没有则查数据库
        catalogJsonFromDb = getCategoriesJsonFromDb();

        // 如果查询结果为空, 则存入一个特定值
        String catalogJsonValue = JSON.toJSONString(catalogJsonFromDb);
        if (null == catalogJsonFromDb || catalogJsonFromDb.isEmpty()) {
            catalogJsonValue = "null";
        }
        // 将查询结果存入缓存
        stringRedisTemplate.opsForValue().set(catalogJsonKey, catalogJsonValue, 10, TimeUnit.MINUTES);

        return catalogJsonFromDb;
    }


    /**
     * 查询分类 + 锁
     *
     * 1. 使用 Synchronized、ReentrantLock 单机锁, 单机情况没问题, 多个服务时还是会出现问题
     * 2. 简单的分布式锁, 如果查询出现异常或者宕机, 没有成功释放锁, 则死锁
     * 3. 添加过期时间, 如果占锁和设置过期时间分开, 不是原子的, 还是有可能出现死锁
     * 4. 使用一行命令占锁+设置过期时间, 可能操作没有执行完, 锁就过期了, 可能释放其他人的锁
     * 5. 释放锁时判断当前锁是否是自己的, 但是查询锁的值和释放锁操作分开, 则不是原子的, 还是有可能释放别人的锁
     *          - 当查询锁对应的 Value1 时锁还没过期
     *          - 在将 Value1 通过网络传输给程序的过程中, Value1 对应的锁过期了, 并且第二个线程抢占了锁, 设值为 Value2
     *          - 当程序得到 Redis 返回的 Value1 时, Redis 中实际存的值为 Value2, 此时释放锁, 就释放了第二个线程的锁
     * 6. 使用 Lua 脚本, 解决释放锁操作的原子性问题
     * 7. 如果业务很复杂, 完成时间难以估计, 在执行完成前锁就释放了, 会产生一定影响, 所以就需要锁的续期
     *
     * 8. 上面的内容自己写太复杂, 使用 RedLock - Redisson
     *
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWhitRedLock(){
        // 先查询缓存, 命中就直接返回
        String catalogJsonKey = "catalogJson";
        String catalogJson = stringRedisTemplate.opsForValue().get(catalogJsonKey);
        Map<String, List<Catelog2Vo>> catalogJsonFromDb;
        if (StringUtils.isNotEmpty(catalogJson)) {
            // 缓存中有就直接使用缓存
            catalogJsonFromDb = JSONObject.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
            return catalogJsonFromDb;
        }

        RLock lock = redisson.getLock(catalogJsonKey+"Lock");
        /**
         * 如果未指定超时时间, 就会使用 30s(看门狗的默认时间)
         * 只要占锁成功, 就会启动一个定时任务, 每过 10s(也就是 1/3 看门狗时间), 就会自动将锁的过期时间重新设置成 30s
         *
         * 如果执行超时间, 就直接使用传入的时间, 通过 Lua 脚本创建锁, 不会自动续期
         *
         * 实际中使用指定过期时间的锁, 可以省略看门狗机制
         */
        lock.lock(30, TimeUnit.SECONDS);
        try {
            // 加锁, 查询之前再确认一次缓存中有没有, 释放锁之前先更新到缓存中
            catalogJsonFromDb = getCatalogJsonWhitRedis();
        } finally {
            lock.unlock();
        }

        return catalogJsonFromDb;
    }


    /**
     * 更新菜单
     *
     * 会产生数据不一致的问题
     * 解决方案:
     *  1. 双写模式: 数据库修改完后, 再修改缓存中的数据
     *      - 在大并发下产生的问题:
     *          线程1 先完成数据库更新, 线程2 后完成, 但是 线程1 网络延时比 线程2 更晚更新 Redis, 导致数据库与Redis不一致
     *          这是暂时性的脏数据的问题, 在数据过期后, 会再从数据库同步
     *
     *  2. 失效模式: 数据库修改完后, 删除缓存
     *      - 在大并发下产生的问题:
     *          线程1 更新数据, 操作正常快速完成, 将 Redis 数据删除, 将 数据更新为 1
     *          线程2 更新数据, 将数据从 1 更新为 2
     *          线程3 查询数据, 线程2、3 同时进行, 线程2更新操作比较慢, 线程3 先查询出了数据 1
     *          线程3 网络延时, 线程2 网络正常, 线程2 先执行了删 Redis 操作, 线程3 后将数据1写入 Redis
     *          还是会导致数据暂时性不一致问题
     *
     *
     *
     * - 解决:
     *   加读写锁, 只有线程1执行完, 才允许线程2操作
     *   如果业务允许暂时性的数据不一致问题, 可以忽略该问题
     *   Canal 订阅 Binlog
     *
     *
     */

    //@CacheEvict(cacheNames = {"category"}, key = "'level1Categorys'") // 删除指定 Key 的缓存

    //@CacheEvict(cacheNames = {"category"}, allEntries = true) // allEntries = true 删除分区下所有数据

    //@Caching(evict = {
    //        @CacheEvict(cacheNames = {"category"}, key = "'level1Categorys'"),
    //        @CacheEvict(cacheNames = {"category"}, key = "'level2Categories'")
    //}) // 批量操作, 批量删除


    public R updateCatalogJson(Category category){
        int update = baseMapper.updateById(category);
        return R.ok(update);
    }
}
